#!/usr/bin/python
# -*- coding: utf-8 -*-

'''
Created on Jun 3, 2010

@author: maraoz
'''

from random import randint, random

from util import random_string
from constants import SESSION_DIRECTORY

__v = 0
def get_next_integer():
    global __v
    __v += 1
    return __v

class Cell(object):
    
    def __init__(self, threshold=100.0, name=None):
        self.inputs = []
        self.threshold = threshold
        
        self.cache = None
        
        self.name = get_next_integer() if not name else name
    
    def erase_memory(self):
        self.cache = None 
    
    def connect_input(self, other_cell, weight=50.0):
        self.inputs.append((other_cell, weight))
    
    def get_inputs(self):
        return self.inputs
    
    def read_value(self):
        if self.cache:
            return self.cache
        sum = 0.0
        for cell, weight in self.inputs:
            sum = sum + cell.read_value() * weight
        self.cache = 1 if sum >= self.threshold else 0
        return self.cache

class Sensor(object):
    def __init__(self, name=None):
        self.name = ("sensor_" + random_string(4)) if not name else name
    
    def get_inputs(self):
        return []

    def read_value(self):
        raise NotImplementedError


class Motor(object):
    name = ""
    def activate(self):
        raise NotImplementedError

def random_brain(sensors, motors):
    b = Brain(sensors, motors)
    b.randomize()
    return b

class Brain(object):
    
    def __init__(self, sensors, motors, name=None):
        self.sensors = sensors
        self.motors = motors
        
        self.name = random_string() if not name else name
        
        self.motor_cells = []
        self.cells = set()
        
    def randomize(self, layer_count=5, max_cells_per_layer=8, density=0.5):
        if not (0 <= density <= 1):
            return None
        last_layer = self.sensors
        for i in xrange(layer_count):
            new_layer = []
            cell_ammount = randint(1, max_cells_per_layer)
            if i == layer_count - 1:
                cell_ammount = len(self.motors)
            for _ in xrange(cell_ammount):
                c = Cell()
                for possible_input in last_layer:
                    if random() < density:
                        c.connect_input(possible_input)
                self.cells.add(c)
                new_layer.append(c)
            last_layer = new_layer
        
        self.motor_cells = last_layer
        for index, motor_cell in enumerate(self.motor_cells):
            motor_cell.name = "motor_" + self.motors[index].name
        

    def draw(self, show_fired=False, suffix=""):
        suff = ("_color" if show_fired else "") + suffix
        fin = open(SESSION_DIRECTORY + "/" + self.name + suff + ".gv", "w")
        fin.write("""/* Brain graph automatically generated by brain.py */\n""")
        fin.write("digraph %s { \n" % self.name)
        visited = set()
        q = list()
        
        for motor_cell in self.motor_cells:
            q.append(motor_cell)
        
        while(len(q) != 0):
            cell = q.pop()
            if cell in visited:
                continue
            
            for in_cell, weight in cell.get_inputs():
                fin.write("""\t"%s" -> "%s" """ % (in_cell.name, cell.name))
                color = ""
                if show_fired and in_cell.read_value() == 1:
                    color = ", color=red"
                fin.write(" [ label = %d%s ];\n" \
                          % (weight, color))

                if in_cell not in q:
                    q.append(in_cell)
            
            visited.add(cell)
        fin.write("}\n")
        fin.close()
    
    def think(self):
        self.erase_memory()
        
        for index, motor_cell in enumerate(self.motor_cells):
            if motor_cell.read_value() == 1:
                self.motors[index].activate()
        
    
    def erase_memory(self):
        for cell in self.cells:
            cell.erase_memory()
    


def main():
    
    class AlwaysOnSensor(Sensor):
        def read_value(self):
            return 1


    class TextMotor(Motor):
        def activate(self):
            print "Motor activated!"
    
    sensors = [AlwaysOnSensor() for _ in xrange(5)]
    motors = [TextMotor() for _ in xrange(5)]
    print "Creating new random brain."
    b = random_brain(sensors, motors)
    b.draw()
    print "Testing brain thinking..."
    b.think()
    print "Saving brain drawing."
    b.draw(show_fired=True)

if __name__ == "__main__":
    main()
